package com.sxhuayuan.parking.service.impl;

import java.beans.PropertyDescriptor;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.FlushModeType;
import javax.persistence.LockModeType;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.FatalBeanException;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.data.domain.Sort.Order;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import com.sxhuayuan.parking.compenent.Filter;
import com.sxhuayuan.parking.compenent.Filter.Operator;
import com.sxhuayuan.parking.compenent.Page;
import com.sxhuayuan.parking.compenent.Pageable;
import com.sxhuayuan.parking.dao.BaseDao;
import com.sxhuayuan.parking.entity.BaseEntity;
import com.sxhuayuan.parking.entity.OrderEntity;
import com.sxhuayuan.parking.service.BaseService;
import com.sxhuayuan.parking.utils.DateUtils;

@Transactional
public abstract class BaseServiceImpl<T extends BaseEntity, ID extends Serializable> implements BaseService<T, ID> {

	/** 更新忽略属性 */
	private static final String[] UPDATE_IGNORE_PROPERTIES = new String[] { BaseEntity.ID_PROPERTY_NAME, BaseEntity.CREATE_DATE_PROPERTY_NAME, BaseEntity.MODIFY_DATE_PROPERTY_NAME };

	public abstract BaseDao<T, ID> getDAO();

	@PersistenceContext
	protected EntityManager entityManager;

	/** 实体类类型 */
	private Class<T> entityClass;

	@SuppressWarnings("unchecked")
	public BaseServiceImpl() {
		Type type = getClass().getGenericSuperclass();
		Type[] parameterizedType = ((ParameterizedType) type).getActualTypeArguments();
		entityClass = (Class<T>) parameterizedType[0];
	}

	@Override
	@Transactional(readOnly = true)
	public T findOne(ID id) {
		return getDAO().findById(id).get();
	}

	@Override
	@Transactional(readOnly = true)
	public List<T> findAll() {
		return getDAO().findAll();
	}

	@Override
	@Transactional(readOnly = true)
	public List<T> findList(@SuppressWarnings("unchecked") ID... ids) {
		return getDAO().findAllById(Arrays.asList(ids));
	}

	@Override
	@Transactional(readOnly = true)
	public List<T> findList(Integer count, List<Filter> filters, List<Order> orders) {
		return findList(0, count, filters, orders);
	}

	@Override
	@Transactional(readOnly = true)
	public List<T> findList(Integer first, Integer count, List<Filter> filters, List<Order> orders) {
		CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
		CriteriaQuery<T> criteriaQuery = criteriaBuilder.createQuery(entityClass);
		criteriaQuery.select(criteriaQuery.from(entityClass));
		return findList(criteriaQuery, first, count, filters, orders);
	}

	@Override
	public Page<T> findPage(Pageable pageable) {
		if (StringUtils.isEmpty(pageable.getOrderProperty())) {
			pageable.setOrderProperty(BaseEntity.ID_PROPERTY_NAME);
		}
		// PageRequest的page是从0开始的
		PageRequest pageRequest = PageRequest.of(pageable.getPageNumber() - 1, pageable.getPageSize(), pageable.getOrderDirection(), pageable.getOrderProperty());
		org.springframework.data.domain.Page<T> page = getDAO().findAll(new Specification<T>() {

			private static final long serialVersionUID = 1L;

			@SuppressWarnings("unchecked")
			@Override
			public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
				return addRestrictions(root, (CriteriaQuery<T>) query, pageable);
			}

		}, pageRequest);
		return new Page<T>(page.getContent(), page.getTotalElements(), pageable);
	}

	@Override
	@Transactional(readOnly = true)
	public Page<T> findPage(Pageable pageable, Specification<T> specification) {
		if (StringUtils.isEmpty(pageable.getOrderProperty())) {
			pageable.setOrderProperty(BaseEntity.ID_PROPERTY_NAME);
		}
		// PageRequest的page是从0开始的
		PageRequest pageRequest = PageRequest.of(pageable.getPageNumber() - 1, pageable.getPageSize(), pageable.getOrderDirection(), pageable.getOrderProperty());
		org.springframework.data.domain.Page<T> page = getDAO().findAll(new Specification<T>() {

			private static final long serialVersionUID = 1L;

			@Override
			public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
				@SuppressWarnings("unchecked")
				Predicate predicates = addRestrictions(root, (CriteriaQuery<T>) query, pageable);

				if (specification != null) {
					Predicate predicate = specification.toPredicate(root, query, criteriaBuilder);
					predicates = criteriaBuilder.and(predicates, predicate);
				}
				return predicates;
			}

		}, pageRequest);
		return new Page<T>(page.getContent(), page.getTotalElements(), pageable);
	}

	@Override
	public Page<T> findPage(String hql, String countHql, int size, int pageNum) {
		pageNum = pageNum <= 0 ? 1 : pageNum;
		TypedQuery<T> query = entityManager.createQuery(hql, entityClass);
		query.setFirstResult(--pageNum * size).setMaxResults(size);
		List<T> resultList = query.getResultList();
		Long total = entityManager.createQuery(countHql, Long.class).setMaxResults(1).getSingleResult();
		Page<T> page = new Page<>(resultList, total, new Pageable(pageNum, size));
		return page;
	}

	@Override
	@Transactional(readOnly = true)
	public long count() {
		return getDAO().count();
	}

	@Override
	public long count(Pageable pageable, Specification<T> specification) {
		return getDAO().count(new Specification<T>() {

			private static final long serialVersionUID = 1L;

			@Override
			public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
				@SuppressWarnings("unchecked")
				Predicate predicates = addRestrictions(root, (CriteriaQuery<T>) query, pageable);

				if (specification != null) {
					Predicate predicate = specification.toPredicate(root, query, criteriaBuilder);
					predicates = criteriaBuilder.and(predicates, predicate);
				}
				return predicates;
			}

		});
	}

	@Override
	@Transactional(readOnly = true)
	public long count(Filter... filters) {
		return getDAO().count(new Specification<T>() {

			private static final long serialVersionUID = 1L;

			@Override
			public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
				CriteriaQuery<T> createQuery = criteriaBuilder.createQuery(entityClass);
				query = createQuery;
				return addRestrictions(createQuery, Arrays.asList(filters));
			}

		});
	}

	@Override
	@Transactional(readOnly = true)
	public boolean exists(ID id) {
		return getDAO().existsById(id);
	}

	@Override
	@Transactional(readOnly = true)
	public boolean exists(Filter... filters) {
		return count(filters) > 0;
	}

	@Override
	public void save(T entity) {
		getDAO().save(entity);
	}

	@Override
	public T update(T entity) {
		return getDAO().save(entity);
	}

	@Override
	public T update(T entity, String... ignoreProperties) {
		Assert.notNull(entity, "");
		if (entityManager.contains(entity)) {
			throw new IllegalArgumentException("Entity must not be managed");
		}
		T persistant = getDAO().findById(getIdentifier(entity)).get();
		if (persistant != null) {
			copyProperties(entity, persistant, (String[]) ArrayUtils.addAll(ignoreProperties, UPDATE_IGNORE_PROPERTIES));
			return update(persistant);
		} else {
			return update(entity);
		}
	}

	@SuppressWarnings("unchecked")
	private ID getIdentifier(T entity) {
		Assert.notNull(entity, "");
		return (ID) entityManager.getEntityManagerFactory().getPersistenceUnitUtil().getIdentifier(entity);
	}

	@Override
	public void delete(ID id) {
		getDAO().deleteById(id);
	}

	@Override
	public void delete(@SuppressWarnings("unchecked") ID... ids) {
		if (ids != null) {
			for (ID id : ids) {
				delete(getDAO().findById(id).get());
			}
		}
	}

	@Override
	public void delete(T entity) {
		getDAO().delete(entity);
	}

	protected List<T> findList(CriteriaQuery<T> criteriaQuery, Integer first, Integer count, List<Filter> filters, List<Order> orders) {
		Assert.notNull(criteriaQuery, "criteriaQuery can't be null");
		Assert.notNull(criteriaQuery.getSelection(), "selection can't be null");
		Assert.notEmpty(criteriaQuery.getRoots(), "roots can't be null");

		CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
		Root<T> root = getRoot(criteriaQuery);
		addRestrictions(criteriaQuery, filters);
		addOrders(criteriaQuery, orders);
		if (criteriaQuery.getOrderList().isEmpty()) {
			if (OrderEntity.class.isAssignableFrom(entityClass)) {
				criteriaQuery.orderBy(criteriaBuilder.asc(root.get(OrderEntity.ORDER_PROPERTY_NAME)));
			} else {
				criteriaQuery.orderBy(criteriaBuilder.desc(root.get(OrderEntity.CREATE_DATE_PROPERTY_NAME)));
			}
		}
		TypedQuery<T> query = entityManager.createQuery(criteriaQuery).setFlushMode(FlushModeType.COMMIT);
		if (first != null) {
			query.setFirstResult(first);
		}
		if (count != null) {
			query.setMaxResults(count);
		}
		return query.getResultList();
	}

	public Predicate addRestrictions(CriteriaQuery<T> criteriaQuery, List<Filter> filters) {
		if (criteriaQuery == null || filters == null || filters.isEmpty()) {
			return null;
		}
		Root<T> root = getRoot(criteriaQuery);
		if (root == null) {
			return null;
		}
		CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
		Predicate restrictions = criteriaQuery.getRestriction() != null ? criteriaQuery.getRestriction() : criteriaBuilder.conjunction();
		for (Filter filter : filters) {
			if (filter == null || StringUtils.isEmpty(filter.getProperty())) {
				continue;
			}
			if (filter.getOperator() == Operator.eq && filter.getValue() != null) {
				if (filter.getIgnoreCase() != null && filter.getIgnoreCase() && filter.getValue() instanceof String) {
					restrictions = criteriaBuilder.and(restrictions,
							criteriaBuilder.equal(criteriaBuilder.lower(root.<String>get(filter.getProperty())), ((String) filter.getValue()).toLowerCase()));
				} else {
					restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.equal(root.get(filter.getProperty()), filter.getValue()));
				}
			} else if (filter.getOperator() == Operator.ne && filter.getValue() != null) {
				if (filter.getIgnoreCase() != null && filter.getIgnoreCase() && filter.getValue() instanceof String) {
					restrictions = criteriaBuilder.and(restrictions,
							criteriaBuilder.notEqual(criteriaBuilder.lower(root.<String>get(filter.getProperty())), ((String) filter.getValue()).toLowerCase()));
				} else {
					restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.notEqual(root.get(filter.getProperty()), filter.getValue()));
				}
			} else if (filter.getOperator() == Operator.gt && filter.getValue() != null) {
				restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.gt(root.<Number>get(filter.getProperty()), (Number) filter.getValue()));
			} else if (filter.getOperator() == Operator.lt && filter.getValue() != null) {
				restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.lt(root.<Number>get(filter.getProperty()), (Number) filter.getValue()));
			} else if (filter.getOperator() == Operator.ge && filter.getValue() != null) {
				restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.ge(root.<Number>get(filter.getProperty()), (Number) filter.getValue()));
			} else if (filter.getOperator() == Operator.le && filter.getValue() != null) {
				restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.le(root.<Number>get(filter.getProperty()), (Number) filter.getValue()));
			} else if (filter.getOperator() == Operator.like && filter.getValue() != null && filter.getValue() instanceof String) {
				restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.like(root.<String>get(filter.getProperty()), (String) filter.getValue()));
			} else if (filter.getOperator() == Operator.in && filter.getValue() != null) {
				restrictions = criteriaBuilder.and(restrictions, root.get(filter.getProperty()).in(filter.getValue()));
			} else if (filter.getOperator() == Operator.isNull) {
				restrictions = criteriaBuilder.and(restrictions, root.get(filter.getProperty()).isNull());
			} else if (filter.getOperator() == Operator.isNotNull) {
				restrictions = criteriaBuilder.and(restrictions, root.get(filter.getProperty()).isNotNull());
			}
		}
		criteriaQuery.where(restrictions);
		return restrictions;
	}

	public Predicate addRestrictions(Root<T> root, CriteriaQuery<?> criteriaQuery, Pageable pageable) {
		if (criteriaQuery == null || pageable == null) {
			return null;
		}
		// 判断
		CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
		// 谓词，断言
		Predicate restrictions = criteriaQuery.getRestriction() != null ? criteriaQuery.getRestriction() : criteriaBuilder.conjunction();
		if (StringUtils.isNotEmpty(pageable.getSearchProperty()) && StringUtils.isNotEmpty(pageable.getSearchValue())) {
			String searchProp = pageable.getSearchProperty();
			// 查询关联表属性
			if (searchProp.indexOf(".") != -1) {
				int lastIndex = searchProp.lastIndexOf(".");
				// 关联表在对象中的名称
				String joinTables = searchProp.substring(0, lastIndex);
				// 关联表的属性名称
				String joinTableAttrName = searchProp.substring(lastIndex + 1);
				String[] tables = joinTables.split("\\.");
				Join<?, ?> join = root.join(tables[0]);
				for (int i = 1; i < tables.length; i++) {
					join = join.join(tables[i]);
				}
				// 关联表的root
				Path<String> joinPath = join.get(joinTableAttrName);
				if (joinPath != null) {
					restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.gt(criteriaBuilder.locate(joinPath, pageable.getSearchValue()), 0));
				}
			} else {
				restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.gt(criteriaBuilder.locate(root.<String>get(pageable.getSearchProperty()), pageable.getSearchValue()), 0));
			}
		}
		if (pageable.getFilters() != null) {
			for (Filter filter : pageable.getFilters()) {
				if (filter == null || StringUtils.isEmpty(filter.getProperty())) {
					continue;
				}
				if (filter.getOperator() == Operator.eq && filter.getValue() != null) {
					if (filter.getIgnoreCase() != null && filter.getIgnoreCase() && filter.getValue() instanceof String) {
						restrictions = criteriaBuilder.and(restrictions,
								criteriaBuilder.equal(criteriaBuilder.lower(root.<String>get(filter.getProperty())), ((String) filter.getValue()).toLowerCase()));
					} else {
						restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.equal(root.get(filter.getProperty()), filter.getValue()));
					}
				} else if (filter.getOperator() == Operator.ne && filter.getValue() != null) {
					if (filter.getIgnoreCase() != null && filter.getIgnoreCase() && filter.getValue() instanceof String) {
						restrictions = criteriaBuilder.and(restrictions,
								criteriaBuilder.notEqual(criteriaBuilder.lower(root.<String>get(filter.getProperty())), ((String) filter.getValue()).toLowerCase()));
					} else {
						restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.notEqual(root.get(filter.getProperty()), filter.getValue()));
					}
				} else if (filter.getOperator() == Operator.gt && filter.getValue() != null) {
					restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.gt(root.get(filter.getProperty()), (Number) filter.getValue()));
				} else if (filter.getOperator() == Operator.lt && filter.getValue() != null) {
					restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.lt(root.<Number>get(filter.getProperty()), (Number) filter.getValue()));
				} else if (filter.getOperator() == Operator.ge && filter.getValue() != null) {
					restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.ge(root.<Number>get(filter.getProperty()), (Number) filter.getValue()));
				} else if (filter.getOperator() == Operator.le && filter.getValue() != null) {
					restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.le(root.<Number>get(filter.getProperty()), (Number) filter.getValue()));
				} else if (filter.getOperator() == Operator.gt_dt && filter.getValue() != null) {
					Date date = filter.getValue() instanceof Date ? (Date) filter.getValue() : DateUtils.parse(filter.getValue().toString());
					restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.greaterThan(root.get(filter.getProperty()), date));
				} else if (filter.getOperator() == Operator.lt_dt && filter.getValue() != null) {
					Date date = filter.getValue() instanceof Date ? (Date) filter.getValue() : DateUtils.parse(filter.getValue().toString());
					restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.lessThan(root.get(filter.getProperty()), date));
				} else if (filter.getOperator() == Operator.ge_dt && filter.getValue() != null) {
					Date date = filter.getValue() instanceof Date ? (Date) filter.getValue() : DateUtils.parse(filter.getValue().toString());
					restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.greaterThanOrEqualTo(root.get(filter.getProperty()), date));
				} else if (filter.getOperator() == Operator.le_dt && filter.getValue() != null) {
					Date date = filter.getValue() instanceof Date ? (Date) filter.getValue() : DateUtils.parse(filter.getValue().toString());
					restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.lessThanOrEqualTo(root.get(filter.getProperty()), date));
				} else if (filter.getOperator() == Operator.like && filter.getValue() != null && filter.getValue() instanceof String) {
					restrictions = criteriaBuilder.and(restrictions, criteriaBuilder.like(root.<String>get(filter.getProperty()), (String) filter.getValue()));
				} else if (filter.getOperator() == Operator.in && filter.getValue() != null) {
					restrictions = criteriaBuilder.and(restrictions, root.get(filter.getProperty()).in(filter.getValue()));
				} else if (filter.getOperator() == Operator.isNull) {
					restrictions = criteriaBuilder.and(restrictions, root.get(filter.getProperty()).isNull());
				} else if (filter.getOperator() == Operator.isNotNull) {
					restrictions = criteriaBuilder.and(restrictions, root.get(filter.getProperty()).isNotNull());
				}
			}
		}
		return restrictions;
	}

	@Override
	public void refresh(T t, LockModeType type) {
		entityManager.refresh(t, type);
	}

	@Override
	public void lock(T t, LockModeType mode) {
		entityManager.lock(t, mode);
	}

	private Root<T> getRoot(CriteriaQuery<T> criteriaQuery) {
		if (criteriaQuery != null) {
			return getRoot(criteriaQuery, criteriaQuery.getResultType());
		}
		return null;
	}

	private Root<T> getRoot(CriteriaQuery<?> criteriaQuery, Class<T> clazz) {
		if (criteriaQuery != null && criteriaQuery.getRoots() != null && clazz != null) {
			for (Root<?> root : criteriaQuery.getRoots()) {
				if (clazz.equals(root.getJavaType())) {
					return (Root<T>) root.as(clazz);
				}
			}
		}
		return null;
	}

	private void addOrders(CriteriaQuery<T> criteriaQuery, List<Order> orders) {
		if (criteriaQuery == null || orders == null || orders.isEmpty()) {
			return;
		}
		Root<T> root = getRoot(criteriaQuery);
		if (root == null) {
			return;
		}
		CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
		List<javax.persistence.criteria.Order> orderList = new ArrayList<javax.persistence.criteria.Order>();
		if (!criteriaQuery.getOrderList().isEmpty()) {
			orderList.addAll(criteriaQuery.getOrderList());
		}
		for (Order order : orders) {
			if (order.getDirection() == Direction.ASC) {
				orderList.add(criteriaBuilder.asc(root.get(order.getProperty())));
			} else if (order.getDirection() == Direction.DESC) {
				orderList.add(criteriaBuilder.desc(root.get(order.getProperty())));
			}
		}
		criteriaQuery.orderBy(orderList);
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	private void copyProperties(Object source, Object target, String[] ignoreProperties) throws BeansException {
		Assert.notNull(source, "Source must not be null");
		Assert.notNull(target, "Target must not be null");

		PropertyDescriptor[] targetPds = BeanUtils.getPropertyDescriptors(target.getClass());
		List<String> ignoreList = (ignoreProperties != null) ? Arrays.asList(ignoreProperties) : null;
		for (PropertyDescriptor targetPd : targetPds) {
			if (targetPd.getWriteMethod() != null && (ignoreProperties == null || (!ignoreList.contains(targetPd.getName())))) {
				PropertyDescriptor sourcePd = BeanUtils.getPropertyDescriptor(source.getClass(), targetPd.getName());
				if (sourcePd != null && sourcePd.getReadMethod() != null) {
					try {
						Method readMethod = sourcePd.getReadMethod();
						if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
							readMethod.setAccessible(true);
						}
						Object sourceValue = readMethod.invoke(source);
						Object targetValue = readMethod.invoke(target);
						if (sourceValue != null && targetValue != null && targetValue instanceof Collection) {
							Collection collection = (Collection) targetValue;
							collection.clear();
							collection.addAll((Collection) sourceValue);
						} else {
							Method writeMethod = targetPd.getWriteMethod();
							if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
								writeMethod.setAccessible(true);
							}
							writeMethod.invoke(target, sourceValue);
						}
					} catch (Throwable ex) {
						throw new FatalBeanException("Could not copy properties from source to target", ex);
					}
				}
			}
		}
	}
}
